﻿using Devil;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace GameToolkit
{
    [TrackColor(0.1f, 1f, 0.2f)]
    [TrackClipType(typeof(PlaybackMark))]
    [TrackBindingType(typeof(PlaybackAction))]
    public class PlaybackTrack : TrackAsset
    {
        public static PlaybackController GetPlaybackController(Playable playable)
        {
            var graph = playable.GetGraph();
            var len = graph.GetRootPlayableCount();
            var lst = CachedList<Playable>.GetList();
            for (int i = 0; i < len; i++)
            {
                var output = graph.GetRootPlayable(i);
                lst.Add(output);
            }
            PlaybackController ctrl = null;
            var index = 0;
            var ctrlType = typeof(PlaybackController);
            while (index < lst.Count)
            {
                var output = lst[index++];
                if (output.IsNull())
                    continue;
                var type = output.GetPlayableType();
                if (type == ctrlType)
                {
                    ctrl = ((ScriptPlayable<PlaybackController>)output).GetBehaviour();
                    break;
                }
                len = output.GetInputCount();
                for (int i = 0; i < len; i++)
                {
                    var play = output.GetInput(i);
                    lst.Add(play);
                }
            }
            CachedList<Playable>.Release(lst);
            return ctrl;
        }

        public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
        {
            var playable = ScriptPlayable<PlaybackController>.Create(graph, inputCount);
            var ctrl = playable.GetBehaviour();
            ctrl.Target = go;
            ctrl.mTrack = this;
            ctrl.mMarkIds = new HashSet<int>();
            ctrl.mMarks = new List<Mark>();
            foreach (var clip in GetClips())
            {
                if (clip.asset is PlaybackMark)
                {
                    ctrl.mMarkIds.Add(clip.displayName.IgnoreCaseToHash());
                    Mark mark = new Mark(clip.displayName, clip.start, true);
                    ctrl.InsertMark(mark);
                    mark = new Mark(clip.displayName, clip.end, false);
                    ctrl.InsertMark(mark);
                }
            }
            return playable;
        }

        internal struct Mark
        {
            public string name;
            public double time;
            public bool isStart;
            public int flag => isStart ? 1 : 2;

            public Mark(string name, double time, bool isStart)
            {
                this.name = name;
                this.time = time;
                this.isStart = isStart;
            }
        }

        public enum ESearchMarkType
        {
            None,
            Next = 0x13,
            NextStart = 0x11,
            NextEnd = 0x12,
            Previours = 0x23,
            PrevStart = 0x21,
            PrevEnd = 0x22,
            Entire = 0x33,
            EntireStart = 0x31,
            EntireEnd = 0x32
        }

        sealed public class PlaybackController: PlayableBehaviour
        {
            HashSet<object> mPauseSources = new HashSet<object>();

            internal PlaybackTrack mTrack;
            Playable mRootPlayable;
            double mDefaultSpeed;
            ESearchMarkType mSearchMark;
            string mSearchName;
            int mNextMarkIndex;
            PlaybackAction mActiveAction;
            internal List<Mark> mMarks;
            internal HashSet<int> mMarkIds;
            internal GameObject Target { get; set; }

            internal void InsertMark(Mark mark)
            {
                var index = 0;
                for (int i = mMarks.Count - 1; i >= 0; i--)
                {
                    if (mark.time >= mMarks[i].time)
                    {
                        index = i + 1;
                        break;
                    }
                }
                if (index < mMarks.Count)
                    mMarks.Insert(index, mark);
                else
                    mMarks.Add(mark);
            }

            public override void OnGraphStart(Playable playable)
            {
                mRootPlayable = playable.GetGraph().GetRootPlayable(0);
                mDefaultSpeed = mRootPlayable.GetSpeed();
                PlaybackAction action = null;
                PlayableDirector dir;
                if (Target != null && mTrack != null && Target.TryGetComponent(out dir) && dir != null)
                    action = dir.GetGenericBinding(mTrack) as PlaybackAction;
                if (action != null)
                {
                    var markType = typeof(PlaybackMark.MarkBehaviour);
                    var count = playable.GetInputCount();
                    for (int i = 0; i < count; i++)
                    {
                        var input = playable.GetInput(i);
                        if (input.GetPlayableType() == markType)
                        {
                            var mark = ((ScriptPlayable<PlaybackMark.MarkBehaviour>)input).GetBehaviour();
                            mark.mAction = action;
                        }
                    }
                }
#if UNITY_EDITOR
                if (Application.isPlaying)
#endif
                    mRootPlayable.SetSpeed(mPauseSources.Count > 0 ? 0 : mDefaultSpeed);
            }

            public override void OnGraphStop(Playable playable)
            {
                mRootPlayable = default;
            }

            public override void OnPlayableDestroy(Playable playable)
            {
                Target = null;
                mRootPlayable = default;
                var action = mActiveAction;
                mActiveAction = null;
                if (action != null)
                    action.Release(this);
            }

            public override void ProcessFrame(Playable playable, FrameData info, object playerData)
            {
                var act = playerData as PlaybackAction;
                if (act != mActiveAction && mActiveAction != null)
                    mActiveAction.Release(this);
                mActiveAction = act;
                if (act != null)
                {
                    act.ProcessFrame(this);
                }
                if (mMarks.Count > 0)
                {
                    var starti = 0;
                    var endi = mMarks.Count - 1;
                    var time = playable.GetTime();
                    mNextMarkIndex = mMarks.Count;
                    while (starti <= endi)
                    {
                        var index = (starti + endi) >> 1;
                        if (mMarks[index].time > time)
                        {
                            mNextMarkIndex = index;
                            endi = index - 1;
                        }
                        else if (mMarks[index].time <= time)
                        {
                            starti = index + 1;
                        }
                    }
                }
                if (mSearchMark != ESearchMarkType.None)
                {
                    Mark mark = default;
                    var next = (int)ESearchMarkType.Next & 0xf0;
                    var prev = (int)ESearchMarkType.Previours & 0xf0;
                    var type = (int)mSearchMark & 0xf0;
                    var flag = (int)mSearchMark & 0xf;
                    if (type == next)
                    {
                        for (int i = mNextMarkIndex; i < mMarks.Count; i++)
                        {
                            if ((flag & mMarks[i].flag) != 0 && mMarks[i].name.EqualsIgnoreCase(mSearchName))
                            {
                                mark = mMarks[i];
                                break;
                            }
                        }
                    }
                    else if (type == prev)
                    {
                        for (int i = mNextMarkIndex - 1; i >= 0; i--)
                        {
                            if ((flag & mMarks[i].flag) != 0 && mMarks[i].name.EqualsIgnoreCase(mSearchName))
                            {
                                mark = mMarks[i];
                                break;
                            }
                        }
                    }
                    else
                    {
                        for (int i = 0; i < mMarks.Count; i++)
                        {
                            var index = (i + mNextMarkIndex) % mMarks.Count;
                            if ((flag & mMarks[i].flag) != 0 && mMarks[index].name.EqualsIgnoreCase(mSearchName))
                            {
                                mark = mMarks[index];
                                break;
                            }
                        }
                    }
                    mSearchMark = ESearchMarkType.None;
                    if (!string.IsNullOrEmpty(mark.name))
                    {
                        mRootPlayable.SetTime(mark.time);
                    }
                }
            }

            public void JumpToMark(string markName, ESearchMarkType searchType)
            {
                if(searchType != ESearchMarkType.None && !string.IsNullOrEmpty(markName) && mMarkIds.Contains(markName.IgnoreCaseToHash()))
                {
                    mSearchMark = searchType;
                    mSearchName = markName;
                }
                else
                {
                    Debug.LogError($"Invalid mark: {markName} (searchType: {searchType})");
                }
            }

            public void Pause(object pauseSource)
            {
                if (pauseSource != null && mPauseSources.Add(pauseSource) && mPauseSources.Count == 1 && !mRootPlayable.IsNull())
                {
#if UNITY_EDITOR
                    var target = Target == null ? "null" : Target.name;
                    if (!Application.isPlaying)
                        Debug.Log($"Pause Timeline ({target})", Target);
                    else
#endif
                        mRootPlayable.SetSpeed(0);
                }
            }

            public void Resume(object pauseSource)
            {
                if (pauseSource != null && mPauseSources.Remove(pauseSource) && mPauseSources.Count == 0 && !mRootPlayable.IsNull())
                {
#if UNITY_EDITOR
                    var target = Target == null ? "null" : Target.name;
                    if (!Application.isPlaying)
                        Debug.Log($"Resume Timeline ({target})", Target);
                    else
#endif
                        mRootPlayable.SetSpeed(mDefaultSpeed);
                }
            }
        }
    }
}
